import logging
import os

log = logging.getLogger('vs_sql')

TABLE_NAME = 'neon_mcast_ver'

#----------------------------------------------------------------------------
class Set:
    def __init__(self, db, vs, table_id, peer_id):
        self.db_ = db
        self.vs_ = vs
        self.table_id_ = table_id
        self.peer_id_  = peer_id
        
    def __iter__(self):
	return SetIter(self)

    def __repr__(self):
	return '<Set %s.%s>' % (self.table_id_, self.peer_id_)

    def max(self):
        sql = ("SELECT max(max) FROM %s WHERE id = '%s' AND peer_id = '%s';" % 
               (TABLE_NAME, self.table_id_, self.peer_id_))
        m1 = self.db_.query(sql)[0][0]
        if m1 is None:
            m1 = 0
        else:
            m1 = int(m1)
        return m1

    def set(self, version, data, merge = False):
        return self.vs_.set_ver(self.peer_id_, version, data, merge)

    def get(self, version):
        return self.vs_.get_ver(self.peer_id_, version)

    def subtract(self, verset):
	"""Returns an iterable sequence SetIter."""
	return SetDiff_Iter(self.db_, self, verset)

    def get_compact_encoding(self):
        sql = ("SELECT data, min, max FROM %s "
               "WHERE id = '%s' and peer_id = '%s';" %
               (TABLE_NAME, self.table_id_, self.peer_id_))
        rows = self.db_.query(sql)
        return rows

    def from_compact_encoding(self, encoding):
        sql = ("DELETE FROM %s WHERE id = '%s' AND peer_id = '%s';" %
               (TABLE_NAME, self.table_id_, self.peer_id_))
        self.db_.exec_sql(sql)
        
        for data, min, max in encoding:
            sql = ("INSERT INTO %s VALUES ('%s', '%s', %d, %d, %d)" %
                   (TABLE_NAME,
                    self.table_id_, self.peer_id_,
                    min, max, data))
            self.db_.exec_sql(sql)

#----------------------------------------------------------------------------
class VerSet:
    """Stores a set in a SQL table. Representation is so-so."""
    
    def __init__(self, db_inst, table_id):
        self.db_ = db_inst
        self.table_id_ = table_id

        # stores the non-durable set_vers
        self.non_durable_set_ = {}

    def __repr__(self):
        return '<VerSet %s>' % self.table_id_

    def keys(self):
        # XXX/bowei -- WTF?
        sql = ("SELECT peer_id from %s WHERE id = '%s' GROUP BY peer_id" %
               (TABLE_NAME, self.table_id_))
        rows = self.db_.query(sql)
        return [r[0] for r in rows]

    def get_ver(self, peer_id, ver):
        if (peer_id, ver) in self.non_durable_set_:
            return self.non_durable_set_[peer_id, ver]
        
        sql = ("SELECT data FROM %s WHERE id = '%s' "
               "AND peer_id = '%s' AND min <= %d AND max > %d;" %
               (TABLE_NAME, self.table_id_, peer_id, ver, ver))
        rows = self.db_.query(sql)
        if len(rows) > 0:
            return rows[0][0]
        else:
            return None
        
    def set_ver(self, peer_id, ver, data, non_durable = False):
        """Set the version information to be data. If durable is True,
        then the data set goes to the database. Otherwise, the data
        set is maintained only in the running application (e.g. for
        unacknowledged data transfers, etc) and will disappear when
        the application shuts down."""

        assert isinstance(data, int)
        assert isinstance(ver, int)

        if non_durable:
            self.non_durable_set_[peer_id, ver] = data
            return

        if (peer_id, ver) in self.non_durable_set_:
            del self.non_durable_set_[peer_id, ver]
        
        sql = ("SELECT min, max, data FROM %s " 
               "WHERE id = '%s' AND peer_id = '%s' AND "
               "min <= %d AND max >= %d" %
               (TABLE_NAME, self.table_id_, peer_id, ver, ver))
        rows = self.db_.query(sql)
        l_exists = False
        if len(rows) > 0:
            l_exists = True
            assert(len(rows) == 1)
            l_min, l_max, l_data = rows[0]

        sql = ("SELECT min, max, data FROM %s "
               "WHERE id = '%s' AND peer_id = '%s' AND min = %d" %
               (TABLE_NAME, self.table_id_, peer_id, ver + 1))
        rows = self.db_.query(sql)
        r_exists = False
        if len(rows) > 0:
            r_exists = True
            assert(len(rows) == 1)
            r_min, r_max, r_data = rows[0]

        # ver \in left
        if l_exists and l_min <= ver and l_max > ver:
            if l_data == data:
                return
            else:
                # XXX/bowei -- split interval has not been tested
                self.split_interval_(peer_id, l_min, l_max, l_data, ver, data)
                return

        # merge with left only
        if l_exists and not r_exists:
            if l_data == data:
                self.extend_interval_(peer_id, l_min, l_max, l_min, ver + 1)
                return

        # merge with right only
        if not l_exists and r_exists:
            if r_data == data:
                self.extend_interval_(peer_id, r_min, r_max, ver, r_max)
                return

        # potentially merge with both
        if l_exists and r_exists:
            if (l_data == data) and (r_data == data):
                self.merge_middle_(peer_id, l_min, l_max, r_min, r_max)
                return
            elif l_data == data:
                self.extend_interval_(peer_id, l_min, l_max, l_min, ver + 1)
                return
            elif r_data == data:
                self.extend_interval_(peer_id, r_min, r_max, ver, r_max)
                return

        # Can't merge with any of the existing spans
        self.add_interval_(peer_id, ver, data)

    def add_interval_(self, peer_id, ver, data):
        sql = "INSERT INTO %s VALUES ('%s', '%s', %d, %d, %d);" % \
              (TABLE_NAME, self.table_id_, peer_id, ver, ver + 1, data)
        self.db_.exec_sql(sql)

    def extend_interval_(self, peer_id, o_min, o_max, n_min, n_max):
        sql = ("UPDATE %s SET min = %d, max = %d "
               "WHERE id = '%s' AND peer_id = '%s' AND min = %d AND max = %d" %
               (TABLE_NAME, n_min, n_max, self.table_id_, peer_id, o_min, o_max))
        self.db_.exec_sql(sql)
        
    def split_interval_(self, peer_id, o_min, o_max, o_data, ver, data):
        assert o_min <= ver and o_max > ver
        self.add_interval_(peer_id, ver, data)
        sql = ("INSERT INTO %s VALUES ('%s', '%s', %d, %d, %d)" %
               (TABLE_NAME, self.table_id_, peer_id, ver + 1, o_max, data))
        self.db_.exec_sql(sql)
        
        sql = ("UPDATE %s SET max = %d "
               "WHERE id = '%s' AND peer_id = '%s' AND min = %d AND max = %d" %
               (TABLE_NAME, ver - 1, self.table_id_, peer_id, o_min, o_max))
        self.db_.exec_sql(sql)
        
    def merge_middle_(self, peer_id, l_min, l_max, r_min, r_max):
        sql = ("DELETE FROM %s "
               "WHERE id = '%s' AND peer_id = '%s' AND min = %d AND max = %d" %
               (TABLE_NAME, self.table_id_, peer_id, r_min, r_max))
        self.db_.exec_sql(sql)
        self.extend_interval_(peer_id, l_min, l_max, l_min, r_max)

    def get(self, peer_id):
        return Set(self.db_, self, self.table_id_, peer_id)

    def __getitem__(self, peer_id):
        return Set(self.db_, self, self.table_id_, peer_id)

    def __contains__(self, peer_id):
        return peer_id in self.keys()

    def from_compact_encoding(self, encoding):
        for vid in encoding.keys():
            self[vid].from_compact_encoding(encoding[vid])

    def get_compact_encoding(self):
        ret = {}
        for id in self.keys():
            ret[str(id)] = self[id].get_compact_encoding()
        return ret

#----------------------------------------------------------------------------
class VerSetFactory:
    """Factory for the VerSets"""
    def __init__(self, db):
        self.db_ = db

    def db(self):
        return self.db_

    def init_db(self, reset):
        if reset:
            log.info('dropping version set database')
            try:
                self.db_.exec_sql('DROP TABLE %s' % TABLE_NAME)
                self.db_.commit()
            except:
                pass
            self.db_.exec_sql('''CREATE TABLE %s (
                                 id      VARCHAR(40),
                                 peer_id VARCHAR(40),
                                 min     INTEGER,
                                 max     INTEGER,
                                 data    INTEGER);
                             ''' % TABLE_NAME)
            self.db_.commit()
        log.info('version set database initialized')

    def peer_set(self, table_id):
        return VerSet(self.db_, table_id)

#----------------------------------------------------------------------------
class SetDiff_Iter:
    def __init__(self, db_inst, s1, s2):
        self.db_ = db_inst
	self.s1_  = s1
	self.s2_  = s2
        self.cur_ = 1

        # XXX/bowei -- lazy about how to implement done, so I will
        # implement as next() and then push the item onto here.
        self.next_item_ = None

    def __iter__(self):
	return self

    def __repr__(self):
	return '<SetDiff_Iter %s %s cur = %d>' % \
               (self.s1_, self.s2_, self.cur_)

    def table_id(self):
        return self.s1_.table_id_

    def peer_id(self):
        return self.s1_.peer_id_

    def next_with_pos(self):
        if self.next_item_ != None:
            i = self.next_item_
            self.next_item_ = None
            return i

	while True:
	    if self.cur_ > max(self.s1_.max(), self.s2_.max()):
		raise StopIteration()

            sql = ("SELECT data FROM %s "
                   "WHERE id = '%s' AND peer_id = '%s' AND min <= %d AND max > %d" %
                   (TABLE_NAME,
                    self.s1_.table_id_, self.s1_.peer_id_,
                    self.cur_, self.cur_))
            a = self.db_.query(sql)
            
            sql = ("SELECT data FROM %s "
                   "WHERE id = '%s' AND peer_id = '%s' AND min <= %d AND max > %d" %
                   (TABLE_NAME,
                    self.s2_.table_id_, self.s2_.peer_id_,
                    self.cur_, self.cur_))
            b = self.db_.query(sql)
            
	    self.cur_ = self.cur_ + 1
            if len(a) > 0 and len(b) == 0:
		return ((self.s1_.peer_id_, self.cur_ - 1), a[0][0]), \
                       self.cur_ - 1
            
    def next(self):
	return self.next_with_pos()[0]

    def reset(self, pos = None):
        """Position is the optional version that caused the reset. If
        this is available, then we only need to go back after far as
        that version."""
        if pos is None:
            self.cur_ = 1
        elif pos < self.cur_:
            self.cur_ = pos
        self.next_item_ = None

    def done(self):
        # XXX/bowei -- this is kind of bogus implementation wise.
        # XXX/bowei -- use yield
        if self.next_item_ != None:
            return False
        try:
            i = self.next_with_pos()
        except StopIteration:
            return True
        self.next_item_ = i
        return False

    def update(self, v):
        if v[0] == self.s1_.peer_id_ and self.cur_ > v[1]:
            self.cur_ = v[1]
            self.next_item_ = None

#----------------------------------------------------------------------------
class VerSetDiff_Iter:
    """Iterates through VerSet A - VerSet B. Goes round robin through
    the versions in various peers."""
    def __init__(self, vs_A, vs_B):
        self.db_ = vs_A.db_
        self.vs_A_ = vs_A
        self.vs_B_ = vs_B

        self.iters_ = []
        self.make_iters_()

    def __iter__(self):
        return self

    def __repr__(self):
        return '<VerSetDiff_Iter %s-%s>' % \
               (self.vs_A_.table_id_, self.vs_B_.table_id_)

    def A_id(self):
        return self.vs_A_.table_id_

    def B_id(self):
        return self.vs_B_.table_id_

    def make_iters_(self):
        ids = self.vs_A_.keys()
        for i in self.vs_B_.keys():
            if i not in ids:
                ids.append(i)
        self.iters_ = [SetDiff_Iter(self.db_, self.vs_A_.get(i),
                                    self.vs_B_.get(i))
                       for i in ids]
        
    def rotate_(self):
        self.iters_.insert(0, self.iters_.pop())

    def next_with_pos(self):
        found = False
        for i in self.iters_:
            try:
                d, p = i.next_with_pos()
                found = True
                break
            except StopIteration: pass
        if not found:
            raise StopIteration
        self.rotate_()
        return d, p
    
    def next(self):
        return self.next_with_pos()[0]

    def reset(self):
        self.make_iters_()

    def done(self):
        for i in self.iters_:
            if not i.done():
                return False
        return True

    def update(self, pv):
        """pv is the peer_id, version of a new update which in being
        added to vs_A"""
        p, v = pv
        found = False
        for i in self.iters_:
            if i.peer_id() == p:
                i.update(pv)
                found = True
                break
        if not found:
            new_iter = SetDiff_Iter(self.db_, self.vs_A_.get(p), self.vs_B_.get(p))
            self.iters_.append(new_iter)
